#include "common/pch.h"

#include "map/mapmgr.h"

#include <format>

#include <cftc/util/sysutils.h>
#include <cftf/mysql/dbpool.h>
#include <cftf/loger/syslog.h>

#include "dbc/structure.h"
#include "map/worldmap.h"
#include "map/dungeonmap.h"
#include "map/battlegroundmap.h"
#include "map/terrainmgr.h"
#include "map/gridmap.h"
#include "map/gridstates.h"
#include "metadata/metamgr.h"
#include "metadata/instance.h"
#include "metadata/creature.h"
#include "metadata/creaturedata.h"
#include "object/player.h"
#include "object/object.h"
#include "object/worldobject.h"
#include "object/transport.h"
#include "world/world.h"

MapMgr::MapMgr()
{
    i_gridCleanUpDelay = World::Instance().getConfig(CONFIG_UINT32_INTERVAL_GRIDCLEAN);
    timemeter_.SetInterval(World::Instance().getConfig(CONFIG_UINT32_INTERVAL_MAPUPDATE));
}

MapMgr::~MapMgr()
{
    for (auto i = m_Transports.begin(); i != m_Transports.end(); ++i)
    {
        delete* i;
    }
    DeleteStateMachine();
}

void MapMgr::Initialize()
{
    InitStateMachine();
    InitMaxInstanceId();
}

void MapMgr::InitStateMachine()
{
    si_GridStates[game::system::grid::GRID_STATE_INVALID] = new InvalidState;
    si_GridStates[game::system::grid::GRID_STATE_ACTIVE] = new ActiveState;
    si_GridStates[game::system::grid::GRID_STATE_IDLE] = new IdleState;
    si_GridStates[game::system::grid::GRID_STATE_REMOVAL] = new RemovalState;
}

void MapMgr::DeleteStateMachine()
{
    delete si_GridStates[game::system::grid::GRID_STATE_INVALID];
    delete si_GridStates[game::system::grid::GRID_STATE_ACTIVE];
    delete si_GridStates[game::system::grid::GRID_STATE_IDLE];
    delete si_GridStates[game::system::grid::GRID_STATE_REMOVAL];
}

void MapMgr::UpdateGridState(game::system::grid::GridState_e state, Map& map, NGridType& ngrid, game::system::grid::GridInfo& ginfo, const unsigned int& x, const unsigned int& y, const unsigned int& t_diff)
{
    // TODO: The grid state array itself is static and therefore 100% safe, however, the data
    // the state classes in it accesses is not, since grids are shared across maps (for example
    // in instances), so some sort of locking will be necessary later.

    si_GridStates[state]->Update(map, ngrid, ginfo, x, y, t_diff);
}

void MapMgr::InitializeVisibilityDistanceInfo()
{
    for (auto iter = map_hash_.begin(); iter != map_hash_.end(); ++iter) {
        (*iter).second->InitVisibilityDistance();
    }
}

void MapMgr::Init()
{
    Initialize();
}

void MapMgr::UnInit()
{

}

std::shared_ptr<Map> MapMgr::CreateMap(unsigned int id, const battle::object::WorldObject* obj)
{
    auto map_meta = dbc::MapEntry::GetElement(id);
    if (!map_meta) {
        return nullptr;
    }
    std::shared_ptr<Map> m;
    if (map_meta->Instanceable()) {
        CFT_ASSERT(obj && obj->GetTypeId() == TYPEID_PLAYER);
        m = CreateInstance(id, (battle::object::Player*)obj);
        if (m) {
            LoadActiveEntities(m.get());
        }
    } else {
        m = FindMap(id);
        if (!m) {
            m =  std::make_shared<WorldMap>(id, i_gridCleanUpDelay);
            map_hash_[cftc::util::MakeInt64(id, 0)] = m;

            LoadActiveEntities(m.get());

            m->CreateInstanceData(true);
        }
    }
    return m;
}

std::shared_ptr<Map> MapMgr::CreateBgMap(unsigned int mapid, BattleGround* bg)
{
    TerrainManager::Instance().LoadTerrain(mapid);
    return (std::shared_ptr<Map>)CreateBattleGroundMap(mapid, GenerateInstanceId(), bg);
}

std::shared_ptr<Map> MapMgr::FindMap(unsigned int mapid, unsigned int instanceId) const
{
    auto itr = map_hash_.find(cftc::util::MakeInt64(mapid, instanceId));
    if (itr == map_hash_.end()) {
        return nullptr;
    }

    if (instanceId == 0 && itr->second->Instanceable()) {
        assert(false);
        return nullptr;
    }

    return itr->second;
}

void MapMgr::DeleteInstance(unsigned int mapid, unsigned int instanceId)
{
    auto itr = map_hash_.find(cftc::util::MakeInt64(mapid, instanceId));
    if (itr != map_hash_.end()) {
        auto pMap = itr->second;
        if (pMap->Instanceable()) {
            pMap->UnloadAll(true);
            map_hash_.erase(itr);
        }
    }
}

void MapMgr::Update(unsigned int diff)
{
    timemeter_.Update(diff);
    if (!timemeter_.Passed()) {
        return;
    }
    for (auto iter = map_hash_.begin(); iter != map_hash_.end(); ++iter) {
#if 1
        iter->second->Update((unsigned int)timemeter_.GetCurrent());
#else
        if (m_updater.activated())
        {
            m_updater.schedule_update(*iter->second, (unsigned int)timemeter_.GetCurrent());
        } else
        {
            iter->second->Update((unsigned int)timemeter_.GetCurrent());
        }
#endif
    }
#if 0
    if (m_updater.activated())
    {
        m_updater.wait();
    }

    for (TransportSet::iterator iter = m_Transports.begin(); iter != m_Transports.end(); ++iter)
    {
        WorldObject::UpdateHelper helper((*iter));
        helper.Update((unsigned int)timemeter_.GetCurrent());
    }

    // remove all maps which can be unloaded
    MapMapType::iterator iter = i_maps.begin();
    while (iter != i_maps.end())
    {
        Map* pMap = iter->second;
        // check if map can be unloaded
        if (pMap->CanUnload((unsigned int)timemeter_.GetCurrent()))
        {
            pMap->UnloadAll(true);
            delete pMap;

            i_maps.erase(iter++);
        }
        else
        {
            ++iter;
        }
    }
#endif
    timemeter_.SetCurrent(0);
}

void MapMgr::RemoveAllObjectsInRemoveList()
{
    for (auto iter = map_hash_.begin(); iter != map_hash_.end(); ++iter) {
        iter->second->RemoveAllObjectsInRemoveList();
    }
}

bool MapMgr::ExistMapAndVMap(unsigned int mapid, float x, float y)
{
    GridPair p = map::grid::ComputeGridPair(x, y);

    int gx = 63 - p.x_coord;
    int gy = 63 - p.y_coord;
    return GridMap::ExistMap(mapid, gx, gy) && GridMap::ExistVMap(mapid, gx, gy);
}

bool MapMgr::IsValidMAP(unsigned int mapid)
{
    auto map_meta = dbc::MapEntry::GetElement(mapid);
    return map_meta && (!map_meta->IsDungeon() || mt::_Instance::GetElement(mapid));
}

void MapMgr::UnloadAll()
{
    for (auto iter = map_hash_.begin(); iter != map_hash_.end(); ++iter) {
        iter->second->UnloadAll(true);
    }

    map_hash_.clear();
#if 0
    TerrainManager::Instance().UnloadAll();

    if (m_updater.activated())
    {
        m_updater.deactivate();
    }
#endif
}

void MapMgr::InitMaxInstanceId()
{
    i_MaxInstanceId = 0;

    cftf::mysql::DBPool::Instance().RawQuery(
        (int)DataSource_e::Character,
        "SELECT MAX(`id`) FROM `instance`",
        {},
        [this](auto rsp)
        {
            if (!rsp->IsOk() || rsp->GetDataSet()->IsEof()) {
                abort();
            }
            i_MaxInstanceId = rsp->GetDataSet()->GetByFieldIdx(0)->Get<decltype(i_MaxInstanceId)>();
        }
    )->Await();
}

unsigned int MapMgr::GetNumInstances()
{
    unsigned int ret = 0;

    for (auto itr = map_hash_.begin(); itr != map_hash_.end(); ++itr) {
        auto map = itr->second;
        if (!map->IsDungeon()) {
            continue;
        }
        ret += 1;
    }

    return ret;
}

unsigned int MapMgr::GetNumPlayersInInstances()
{
    unsigned int ret = 0;

    for (auto itr = map_hash_.begin(); itr != map_hash_.end(); ++itr) {
        auto map = itr->second;
        if (!map->IsDungeon()) {
            continue;
        }
#if 0
        ret += map->GetPlayers().getSize();
#endif
    }

    return ret;
}

///// returns a new or existing Instance
///// in case of battlegrounds it will only return an existing map, those maps are created by bg-system
std::shared_ptr<Map> MapMgr::CreateInstance(unsigned int id, battle::object::Player* player)
{
    std::shared_ptr<Map> map = nullptr;
    std::shared_ptr<Map> pNewMap;
    unsigned int NewInstanceId = 0;     
    // instanceId of the resulting map
    auto entry = dbc::MapEntry::GetElement(id);

    if (entry->IsBattleGround()) {
        // find existing bg map for player
        NewInstanceId = player->GetBattleGroundId();
        CFT_ASSERT(NewInstanceId);
        map = FindMap(id, NewInstanceId);
        CFT_ASSERT(map);
    } else if (auto pSave = player->GetBoundInstanceSaveForSelfOrGroup(id)) {
        // solo/perm/group
#if 0
        NewInstanceId = pSave->GetInstanceId();
#endif
        map = FindMap(id, NewInstanceId);
        // it is possible that the save exists but the map doesn't
        if (!map)
        {
            pNewMap = CreateDungeonMap(id, NewInstanceId, pSave);
        }
    } else {
        // if no instanceId via group members or instance saves is found
        // the instance will be created for the first time
        NewInstanceId = GenerateInstanceId();

        pNewMap = CreateDungeonMap(id, NewInstanceId);
    }

    // add a new map object into the registry
    if (pNewMap) {
        map_hash_[cftc::util::MakeInt64(id, NewInstanceId)] = pNewMap;
        map = pNewMap;
    }
    return map;
}

std::shared_ptr<DungeonMap> MapMgr::CreateDungeonMap(unsigned int id, unsigned int InstanceId, DungeonPersistentState* save)
{
    // make sure we have a valid map id
    auto entry = dbc::MapEntry::GetElement(id);
    if (!entry) {
        cftf::loger::SysLog::Instance().Error(std::format("CreateDungeonMap: no entry for map {}", id));
        CFT_ASSERT(false);
    }
    if (!mt::_Instance::GetElement(id)) {
        cftf::loger::SysLog::Instance().Error(std::format("CreateDungeonMap: no instance template for map {}", id));
        CFT_ASSERT(false);
    }

    cftf::loger::SysLog::Instance().Deubg(std::format("MapInstanced::CreateInstanceMap: {} map instance {} for {} created", save ? "" : "new ", InstanceId, id));

    auto map = std::make_shared<DungeonMap>(id, i_gridCleanUpDelay, InstanceId);

    // Dungeons can have saved instance data
    bool load_data = save != nullptr;
    map->CreateInstanceData(load_data);

    return map;
}

std::shared_ptr<BattleGroundMap> MapMgr::CreateBattleGroundMap(unsigned int id, unsigned int InstanceId, BattleGround* bg)
{
    cftf::loger::SysLog::Instance().Deubg(std::format("MapInstanced::CreateBattleGroundMap: instance:{} for map:%d and bgType:{} created.", InstanceId, id, bg->GetTypeID()));

    auto map = std::make_shared<BattleGroundMap>(id, i_gridCleanUpDelay, InstanceId);
#if 0
    CFT_ASSERT(map->IsBattleGround());
    map->SetBG(bg);
    bg->SetBgMap(map);
#endif

    // add map into map container
    map_hash_[cftc::util::MakeInt64(id, InstanceId)] = map;

    // BGs/Arenas not have saved instance data
    map->CreateInstanceData(false);

    return map;
}

void MapMgr::LoadContinents()
{
    unsigned int continents[] = { 0, 1, 369 };
    std::shared_ptr<Map> _map = nullptr;

    for (std::size_t i = 0; i < sizeof(continents) / sizeof(continents[0]); ++i) {
        _map = FindMap(continents[i]);

        if (!_map) {
            _map = CreateMap(continents[i], nullptr);
        }

        if (!_map) {
            cftf::loger::SysLog::Instance().Error(std::format("MapManager::LoadContinents() - Unable to create map {}", continents[i]));
        }
    }
    return;
}

void MapMgr::LoadActiveEntities(Map* m)
{
    // Create all local transporters for this map
    m->LoadLocalTransports();

    // Load grids for all objects on this map, if configured so
    if (World::Instance().isForceLoadMap(m->GetId())) {
        mt::CreatureData::Traverse(
            [&m](const mt::CreatureData* c, bool& stop) -> bool
            {
                m->ForceLoadGrid(c->posX, c->posY);
                return true;
            }
        );
    } else { // Normal case - load only grids for npcs that are active    
        auto bounds = mt::CreatureData::active_creature_guids_onmap_.equal_range(m->GetId());
        for (auto itr = bounds.first; itr != bounds.second; ++itr)
        {
            auto data = mt::CreatureData::GetElement(itr->second);
            m->ForceLoadGrid(data->posX, data->posY);
        }
    }
}

void MapMgr::SetMapUpdateInterval(unsigned int t)
{
    if (t > MIN_MAP_UPDATE_DELAY) {
        t = MIN_MAP_UPDATE_DELAY;
    }

    timemeter_.SetInterval(t);
    timemeter_.Reset();
}

void MapMgr::SetGridCleanUpDelay(unsigned int t)
{
    if (t < MIN_GRID_DELAY) {
        i_gridCleanUpDelay = MIN_GRID_DELAY;
    } else {
        i_gridCleanUpDelay = t;
    }
}

float MapMgr::NormalizeOrientation(float o)
{
    // fmod only supports positive numbers. Thus we have
    // to emulate negative numbers
    if (o < 0)
    {
        float mod = o * -1;
        mod = fmod(mod, 2.0f * cftc::CFT_PI);
        mod = -mod + 2.0f * cftc::CFT_PI;
        return mod;
    }
    return fmod(o, 2.0f * cftc::CFT_PI);
}

bool MapMgr::IsValidMapCoord(battle::object::WorldLocation const& loc)
{
    return IsValidMapCoord(loc.mapid, loc.coord_x, loc.coord_y, loc.coord_z, loc.orientation);
}

void MapMgr::LoadTransports()
{
#if 0
#endif
}
